package com.demo.base.utils;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 自定义 ID 生成器
 * ID 生成规则: ID长达 64 bits
 * <p>
 * Simpleflake 算法，Twitter Snowflake变种
 * 参考 https://github.com/jschwartz73/simpleflake-java
 * Created by ethan on 2017/2/6.
 * <p>
 * 1 bits: 符号位（0） |41 bits: Timestamp (毫秒) | 22 bits: 随机数 |
 */

public class IdGenerator {

    private static final int TIME_SHIFT = 22;
    private static final int RANDOM_SHIFT = 42;
    private static final long EPOCH = 1483228800000L;
    private static final long MAX_SIGNED_LONG = 2199023255551L;
    private static final IdGenerator instance = new IdGenerator();

    private long lastTimestamp = 0;
    private Set<Long> recentRandoms = new HashSet<>(5000);

    public static synchronized long generate() {
        return instance.generateId();
    }

    /**
     * 生成唯一ID并转为字符串
     */
    public static synchronized String generateCode() {
        return Long.toString(generate());
    }

    /**
     * Generates a Twitter Snowflake compatible id utilizing randomness for the right most 22 bits and
     * the Twitter standard EPOCH
     *
     * @return long
     */
    private long generateId() {

        long currentTimestamp = System.currentTimeMillis();

        while (lastTimestamp > currentTimestamp) {
            // Clock is running backwards so wait until it isn't
            currentTimestamp = System.currentTimeMillis();
        }

        if (currentTimestamp < EPOCH || currentTimestamp > MAX_SIGNED_LONG) {
            // The current time cannot be less than the EPOCH
            throw new RuntimeException("Invalid System Clock was " + new Date(currentTimestamp));
        }

        final long customTimestamp = currentTimestamp - EPOCH;

        final long shiftedTimestamp = customTimestamp << TIME_SHIFT;

        long random = nextRandomPart();

        if (lastTimestamp != currentTimestamp) {
            // timestamp has advanced so reset it and clear the previous redis
            lastTimestamp = currentTimestamp;
            recentRandoms.clear();
        } else {
            // Same timestamp as previous keep generating randoms till new is found
            while (recentRandoms.contains(random)) {
                random = nextRandomPart();
            }
        }
        recentRandoms.add(random);
        return Long.MAX_VALUE & (shiftedTimestamp | random);
    }

    private long nextRandomPart() {
        return ThreadLocalRandom.current().nextLong() >>> RANDOM_SHIFT;
    }
}
